feat(guest-agent): let Attest return the boot-time GPU attestation evidence - #1111
Merged
Conversation
`Attest` takes a new `AttestArgs` with `include_gpu_evidence`, and `AttestResponse` gains `gpu_evidence` carrying the same boot-time nvattest bytes `GpuInfo` serves. A verifier that wants to check a GPU launch needs the quote, the runtime event log and that evidence together; it can now get all three in one round trip instead of pairing `Attest` with a second `GpuInfo` call. `Attest` gets its own request message rather than a flag on the shared `RawQuoteArgs`, which `GetQuote` and the legacy `Tappd.RawQuote` also use and which have no business growing a GPU option. `AttestArgs` keeps `report_data` at field 1 so the wire format is unchanged for existing callers, and reserves 2 and 3: those numbers carried `include_ccel` and `include_preimages` while this RPC took a `RawQuoteArgs`, and both were bools, so a new bool there would read a pre-0.6.0 client's `include_ccel = true` as something else entirely. The flag is handled in the RPC layer rather than the platform backend because the evidence is a file the boot wrote, not a platform quote. It stays opt-in so callers that do not care about GPUs neither pay the disk read nor carry the payload.
Each SDK grows the request flag and the response field without breaking an existing call site, using whatever that language already does for optional arguments: Rust adds `attest_with(AttestConfig)` alongside `attest`, built with the same `bon` builder `get_tls_key` uses; Go adds `AttestWithOptions` and `Attest` delegates to it with a zero-value `AttestOptions`; Python and JS take a defaulted parameter. All four default `gpu_evidence` to an empty string when the server omits it, so a new SDK still works against an agent that predates the field.
The attestation docs told verifiers to fetch GPU evidence from `GpuInfo`. `Attest` now serves the same bytes, so point at both and keep the binding procedure unchanged: the evidence is authenticated by `evidence_sha256` in the measured `gpu-attestation` event either way, never by `report_data`.
`Attest(report_data, include_gpu_evidence = true)` reads as though the GPU evidence answers the caller's challenge. It does not: nvattest ran at boot against its own nonce, and the response carries a historical record. That misreading is the specific hazard the GPU evidence design warns about -- a relying party believing a GPU figure says more than it does -- so put the answer in the name rather than only in a comment. The proto now also states why sampling the GPU at attestation time would not be an improvement: an NVIDIA report binds the device and a nonce but not the TD the device is attached to, so a fresh report can be relayed from a genuine remote GPU.
…from the backend `PlatformBackend::attest_response` handed back an `AttestResponse` it could not fully populate: the GPU evidence is a file the boot wrote, so every backend wrote `boottime_gpu_evidence: String::new()` and the RPC layer patched the struct afterwards. Three implementations had to remember the blank field, and the platform abstraction knew about a field no platform supplies. `attestation_for_report_data` returns a `VersionedAttestation` instead, which is what the two neighbouring methods on the trait already do. Encoding it and assembling the response is the RPC layer's job, so `Attest` builds its own response in one expression and `AttestAppKey` states plainly that a key attestation carries no machine evidence.
`attestation_for_report_data` described its argument, not its job. The method attests the CVM, which is the distinction that matters once attesting a GPU is also a thing the agent can be asked to do.
kvinwang
force-pushed
the
feat/attest-gpu-evidence
branch
from
August 24, 2026 03:33
06ced79 to
9b0c4f3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Verifying a GPU launch takes three things: the quote, the runtime event log, and the boot-time nvattest evidence whose digest the measured
gpu-attestationevent commits to.Attestreturns the first two. The evidence only comes from a separateGpuInfocall.That split is awkward for the party that needs it. A verifier issues a challenge, gets an attestation back, then has to call a different method for the bytes that the attestation it just received is what authenticates. The two responses share no framing, so nothing in the protocol says they came from the same agent or the same boot.
Fix
Attesttakes a newAttestArgswithinclude_boottime_gpu_evidence, andAttestResponsegainsboottime_gpu_evidencecarrying the same bytesGpuInfoserves.Why a new request message.
AttestsharedRawQuoteArgswithGetQuoteand the legacyTappd.RawQuote, neither of which has any business growing a GPU option.AttestArgskeepsreport_dataat field 1, so the wire format is unchanged for every existing caller, and reserves 2 and 3 — those numbers carriedinclude_ccelandinclude_preimageswhile this RPC took aRawQuoteArgs(a55fdc8e24,2d59979373,3ee183bac2). Both were bools, so a new bool at either would make a pre-0.6.0 client'sinclude_ccel = truearrive as a request for GPU evidence. The flag goes at 4.Why
boottime_in the name.include_gpu_evidencewas the first spelling, and it reads as though the evidence answers the caller'sreport_data. It does not — nvattest ran at boot against its own nonce. That misreading is precisely the hazarddocs/design/attest-deployment.md§8 warns about, so the name now carries the answer instead of only the comment.It is worth being explicit that sampling the GPU at attestation time would not be the stronger alternative it appears to be. An NVIDIA attestation report binds the GPU's identity and a nonce, but says nothing about which TD the device is attached to, so a fresh report can be relayed from a genuine remote GPU — deriving the nonce from the CPU quote does not help, because the relay can compute that nonce too. Only TDISP/TEE-IO device binding closes it. The proto says this at the field.
Why the RPC layer, not the platform backend. The evidence is a file the boot wrote, not a platform quote, so
PlatformBackendstays unaware of it. It is opt-in, so callers that do not care about GPUs neither pay the disk read nor carry the payload.Worker.AttestAppKeysharesAttestResponseand leaves the field empty; the proto notes onlyDstackGuest.Attestpopulates it.SDK parity is included: Rust
attest_with(AttestConfig)next toattest(samebonbuilderget_tls_keyuses), GoAttestWithOptionswithAttestdelegating to a zero-valueAttestOptions, Python and JS defaulted parameters. No existing call site changes. All four default the field to""when the server omits it, so a new SDK still works against an agent that predates it.Verification
Full
dstack/run-tests.sh: 0 failures, re-run after the rename.cargo clippy --workspace --all-features -- -D warnings --allow unused_variablesclean;cargo fmt --check,go vet,gofmt -l,tsc --noEmitclean.End-to-end against the simulator, with an evidence file planted at
/run/nvidia-gpu-attestation/attestation.out:boottime_gpu_evidence{"report_data":"1234"}""{"report_data":"1234","include_boottime_gpu_evidence":true}{"result_code":0,...}GET /Attest?report_data=1234&include_boottime_gpu_evidence=true{"result_code":0,...}GET /GpuInfo(for comparison)A request with no flag key at all — the shape every existing client sends — still returns 200 with the field empty.
SDK suites against the same simulator: Rust 14/14 + 9/9 tappd, Go all pass, JS 36/36. Python's suite needs the
evidence-apidependency, which is not installable in my sandbox; I verified the change directly instead — sync/async signature parity holds (whattests/test_typing.pyasserts), the flag is forwarded, and the field parses back. New tests cover both the flag-set and flag-omitted paths in the guest-agent and in every SDK.